﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Interactivity;
using Kinect.Framework.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace Kinect.Framework.AttachProperties
{
    public class KinectButtonTiltEffect : DependencyObject
    {
        #region Fields and simple properties
        /// <summary>
        /// Delay between releasing an element and the tilt release animation playing
        /// </summary>
        static readonly TimeSpan TiltReturnAnimationDelay = TimeSpan.FromMilliseconds(0);

        /// <summary>
        /// Duration of tilt release animation
        /// </summary>
        static readonly TimeSpan TiltReturnAnimationDuration = TimeSpan.FromMilliseconds(100);

        // These constants are the same as the built-in effects
        /// <summary>
        /// Maximum amount of tilt, in radians
        /// </summary>
        const double MaxAngle = 0.3;

        /// <summary>
        /// Maximum amount of depression, in pixels
        /// </summary>
        const double MaxDepression = 25;
        #endregion

        /// <summary>
        /// Whether the tilt effect is enabled on a container (and all its children)
        /// </summary>
        public static readonly DependencyProperty IsTiltEnabledProperty = DependencyProperty.RegisterAttached(
          "IsTiltEnabled",
          typeof(bool),
          typeof(KinectButtonTiltEffect),
          new PropertyMetadata(OnIsTiltEnabledChanged)
          );
        /// <summary>
        /// Gets the IsTiltEnabled dependency property from an object
        /// </summary>
        /// <param name="source">The object to get the property from</param>
        /// <returns>The property's value</returns>
        public static bool GetIsTiltEnabled(DependencyObject source) { return (bool)source.GetValue(IsTiltEnabledProperty); }

        /// <summary>
        /// Sets the IsTiltEnabled dependency property on an object
        /// </summary>
        /// <param name="source">The object to set the property on</param>
        /// <param name="value">The value to set</param>
        public static void SetIsTiltEnabled(DependencyObject source, bool value) { source.SetValue(IsTiltEnabledProperty, value); }

        /// <summary>
        /// Property change handler for the IsTiltEnabled dependency property
        /// </summary>
        /// <param name="target">The element that the property is atteched to</param>
        /// <param name="args">Event args</param>
        /// <remarks>
        /// Adds or removes event handlers from the element that has (un)registered for tilting
        /// </remarks>
        static void OnIsTiltEnabledChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
        {
            if (target is FrameworkElement)
            {
                // Add / remove our event handler if necessary
                if ((bool)args.NewValue == true)
                {
                    (target as KinectButton).GotKinectFocused += new KinectButton.GotKinectFocusedEventHandler(OnGotKinectFocused);
                    (target as KinectButton).LostKinectFocused += new KinectButton.LostKinectFocusedEventHandler(OnLostKinectFocused);
                }
                else
                {
                    (target as KinectButton).GotKinectFocused -= new KinectButton.GotKinectFocusedEventHandler(OnGotKinectFocused);
                    (target as KinectButton).LostKinectFocused -= new KinectButton.LostKinectFocusedEventHandler(OnLostKinectFocused);
                }
            }
        }

        static void OnLostKinectFocused(object s, Lib.PayloadEventArgs<Interfaces.IKinectElement> e)
        {

            var element = ((KinectButton)e.Payload);
            ResetTiltEffect(element);
        }

        static void OnGotKinectFocused(object s, Lib.PayloadEventArgs<Interfaces.IKinectElement> e)
        {

            var element = ((KinectButton)e.Payload);
            Point tiltTouchPoint;
            try
            {
                tiltTouchPoint = KinectWindow.GetWindow(element).TransformToVisual(element).Transform(new Point(element.LastTouchPoint.X, element.LastTouchPoint.Y));
            }
            catch (Exception excep)
            {
                return;
            }

            // center of the element being tilted
            Point elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);

            // Camera adjustment
            Point centerToCenterDelta = GetCenterToCenterDelta((FrameworkElement)element, (FrameworkElement)KinectWindow.GetWindow(element));

            var prepare = PrepareControlForTilt(element, centerToCenterDelta);
            ApplyTiltEffect(element, tiltTouchPoint, elementCenter);
            return;
        }
        static bool PrepareControlForTilt(FrameworkElement element, Point centerDelta)
        {
            // Don't clobber any existing transforms
            if ((element.RenderTransform != null && element.RenderTransform.GetType() != typeof(MatrixTransform)))
                return false;

            //Console.WriteLine("X = " + centerDelta.X);
            TranslateTransform transform = new TranslateTransform();
            transform.X = centerDelta.X;
            transform.Y = centerDelta.Y;
            element.RenderTransform = transform;

            PlaneProjection projection = new PlaneProjection();
            projection.GlobalOffsetX = 0;
            projection.GlobalOffsetY = 0;
            System.Windows.Media.PlaneProjection.SetPlaneProjection(element, projection);
            return true;
        }
        static Point GetCenterToCenterDelta(FrameworkElement element, FrameworkElement container)
        {
            Point elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
            Point containerCenter;
            containerCenter = new Point(container.ActualWidth / 2, container.ActualHeight / 2);

            Point transformedElementCenter = element.TransformToVisual(container).Transform(elementCenter);
            Point result = new Point(containerCenter.X - transformedElementCenter.X, containerCenter.Y - transformedElementCenter.Y);
            return result;
        }
        static void ResetTiltEffect(FrameworkElement element)
        {
            PlaneProjection projection = System.Windows.Media.PlaneProjection.GetPlaneProjection(element) as PlaneProjection;
            if (projection != null)
            {
                if (
                    projection.GlobalOffsetZ != 0
                    || projection.RotationX != 0
                    || projection.RotationY != 0
                    )
                {
                    var tiltReturnStoryboard = new Storyboard();

                    var tiltReturnXAnimation = new DoubleAnimation();
                    Storyboard.SetTargetProperty(tiltReturnXAnimation, new PropertyPath(PlaneProjection.RotationXProperty));
                    tiltReturnXAnimation.BeginTime = TiltReturnAnimationDelay;
                    tiltReturnXAnimation.From = projection.RotationX;
                    tiltReturnXAnimation.To = 0;
                    tiltReturnXAnimation.Duration = TiltReturnAnimationDuration;

                    var tiltReturnYAnimation = new DoubleAnimation();
                    Storyboard.SetTargetProperty(tiltReturnYAnimation, new PropertyPath(PlaneProjection.RotationYProperty));
                    tiltReturnYAnimation.BeginTime = TiltReturnAnimationDelay;
                    tiltReturnYAnimation.To = 0;
                    tiltReturnYAnimation.From = projection.RotationY;
                    tiltReturnYAnimation.Duration = TiltReturnAnimationDuration;

                    var tiltReturnZAnimation = new DoubleAnimation();
                    Storyboard.SetTargetProperty(tiltReturnZAnimation, new PropertyPath(PlaneProjection.GlobalOffsetZProperty));
                    tiltReturnZAnimation.BeginTime = TiltReturnAnimationDelay;
                    tiltReturnZAnimation.To = 0;
                    tiltReturnYAnimation.From = projection.GlobalOffsetZ;
                    tiltReturnZAnimation.Duration = TiltReturnAnimationDuration;

                    tiltReturnStoryboard.Children.Add(tiltReturnXAnimation);
                    tiltReturnStoryboard.Children.Add(tiltReturnYAnimation);
                    tiltReturnStoryboard.Children.Add(tiltReturnZAnimation);
                    Storyboard.SetTarget(tiltReturnXAnimation, projection);
                    Storyboard.SetTarget(tiltReturnYAnimation, projection);
                    Storyboard.SetTarget(tiltReturnZAnimation, projection);
                    tiltReturnStoryboard.Begin();
                }
            }
        }
        static void ApplyTiltEffect(FrameworkElement element, Point touchPoint, Point centerPoint)
        {
            // Get relative point of the touch in percentage of container size
            Point normalizedPoint = new Point(
                Math.Min(Math.Max(touchPoint.X / (centerPoint.X * 2), 0), 1),
                Math.Min(Math.Max(touchPoint.Y / (centerPoint.Y * 2), 0), 1));

            // Magic math from shell...
            double xMagnitude = Math.Abs(normalizedPoint.X - 0.5);
            double yMagnitude = Math.Abs(normalizedPoint.Y - 0.5);
            double xDirection = -Math.Sign(normalizedPoint.X - 0.5);
            double yDirection = Math.Sign(normalizedPoint.Y - 0.5);
            double angleMagnitude = xMagnitude + yMagnitude;
            double xAngleContribution = xMagnitude + yMagnitude > 0 ? xMagnitude / (xMagnitude + yMagnitude) : 0;

            double angle = angleMagnitude * MaxAngle * 180 / Math.PI;
            double depression = (1 - angleMagnitude) * MaxDepression;

            // RotationX and RotationY are the angles of rotations about the x- or y-*axis*;
            // to achieve a rotation in the x- or y-*direction*, we need to swap the two.
            // That is, a rotation to the left about the y-axis is a rotation to the left in the x-direction,
            // and a rotation up about the x-axis is a rotation up in the y-direction.
            PlaneProjection projection = System.Windows.Media.PlaneProjection.GetPlaneProjection(element) as PlaneProjection;
            projection.RotationY = angle * xAngleContribution * xDirection;
            projection.RotationX = angle * (1 - xAngleContribution) * yDirection;
            projection.GlobalOffsetZ = -depression;
        }
    }
}